/**
 * @license
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import '../../../scripts/bundled-polymer.js';

import '../../../behaviors/base-url-behavior/base-url-behavior.js';
import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
import '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
import '../../core/gr-reporting/gr-reporting.js';
import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
import '../../shared/gr-account-chip/gr-account-chip.js';
import '../../shared/gr-textarea/gr-textarea.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-formatted-text/gr-formatted-text.js';
import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
import '../../shared/gr-overlay/gr-overlay.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../../shared/gr-storage/gr-storage.js';
import '../../shared/gr-account-list/gr-account-list.js';
import '../gr-label-scores/gr-label-scores.js';
import '../gr-thread-list/gr-thread-list.js';
import '../../../styles/shared-styles.js';
import '../gr-comment-list/gr-comment-list.js';
import '../../../scripts/gr-display-name-utils/gr-display-name-utils.js';
import '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-reply-dialog_html.js';

const STORAGE_DEBOUNCE_INTERVAL_MS = 400;

const FocusTarget = {
  ANY: 'any',
  BODY: 'body',
  CCS: 'cc',
  REVIEWERS: 'reviewers',
};

const ReviewerTypes = {
  REVIEWER: 'REVIEWER',
  CC: 'CC',
};

const LatestPatchState = {
  LATEST: 'latest',
  CHECKING: 'checking',
  NOT_LATEST: 'not-latest',
};

const ButtonLabels = {
  START_REVIEW: 'Start review',
  SEND: 'Send',
};

const ButtonTooltips = {
  SAVE: 'Save but do not send notification or change review state',
  START_REVIEW: 'Mark as ready for review and send reply',
  SEND: 'Send reply',
};

const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.';

const SEND_REPLY_TIMING_LABEL = 'SendReply';

/**
 * @appliesMixin Gerrit.BaseUrlMixin
 * @appliesMixin Gerrit.FireMixin
 * @appliesMixin Gerrit.KeyboardShortcutMixin
 * @appliesMixin Gerrit.PatchSetMixin
 * @appliesMixin Gerrit.RESTClientMixin
 * @extends Polymer.Element
 */
class GrReplyDialog extends mixinBehaviors( [
  Gerrit.BaseUrlBehavior,
  Gerrit.FireBehavior,
  Gerrit.KeyboardShortcutBehavior,
  Gerrit.PatchSetBehavior,
  Gerrit.RESTClientBehavior,
], GestureEventListeners(
    LegacyElementMixin(
        PolymerElement))) {
  static get template() { return htmlTemplate; }

  static get is() { return 'gr-reply-dialog'; }
  /**
   * Fired when a reply is successfully sent.
   *
   * @event send
   */

  /**
   * Fired when the user presses the cancel button.
   *
   * @event cancel
   */

  /**
   * Fired when the main textarea's value changes, which may have triggered
   * a change in size for the dialog.
   *
   * @event autogrow
   */

  /**
   * Fires to show an alert when a send is attempted on the non-latest patch.
   *
   * @event show-alert
   */

  /**
   * Fires when the reply dialog believes that the server side diff drafts
   * have been updated and need to be refreshed.
   *
   * @event comment-refresh
   */

  /**
   * Fires when the state of the send button (enabled/disabled) changes.
   *
   * @event send-disabled-changed
   */

  constructor() {
    super();
    this.FocusTarget = FocusTarget;
  }

  static get properties() {
    return {
    /**
     * @type {{ _number: number, removable_reviewers: Array }}
     */
      change: Object,
      patchNum: String,
      canBeStarted: {
        type: Boolean,
        value: false,
      },
      disabled: {
        type: Boolean,
        value: false,
        reflectToAttribute: true,
      },
      draft: {
        type: String,
        value: '',
        observer: '_draftChanged',
      },
      quote: {
        type: String,
        value: '',
      },
      /** @type {!Function} */
      filterReviewerSuggestion: {
        type: Function,
        value() {
          return this._filterReviewerSuggestionGenerator(false);
        },
      },
      /** @type {!Function} */
      filterCCSuggestion: {
        type: Function,
        value() {
          return this._filterReviewerSuggestionGenerator(true);
        },
      },
      permittedLabels: Object,
      /**
       * @type {{ commentlinks: Array }}
       */
      projectConfig: Object,
      knownLatestState: String,
      underReview: {
        type: Boolean,
        value: true,
      },

      _account: Object,
      _ccs: Array,
      /** @type {?Object} */
      _ccPendingConfirmation: {
        type: Object,
        observer: '_reviewerPendingConfirmationUpdated',
      },
      _messagePlaceholder: {
        type: String,
        computed: '_computeMessagePlaceholder(canBeStarted)',
      },
      _owner: Object,
      /** @type {?} */
      _pendingConfirmationDetails: Object,
      _includeComments: {
        type: Boolean,
        value: true,
      },
      _reviewers: Array,
      /** @type {?Object} */
      _reviewerPendingConfirmation: {
        type: Object,
        observer: '_reviewerPendingConfirmationUpdated',
      },
      _previewFormatting: {
        type: Boolean,
        value: false,
        observer: '_handleHeightChanged',
      },
      _reviewersPendingRemove: {
        type: Object,
        value: {
          CC: [],
          REVIEWER: [],
        },
      },
      _sendButtonLabel: {
        type: String,
        computed: '_computeSendButtonLabel(canBeStarted)',
      },
      _savingComments: Boolean,
      _reviewersMutated: {
        type: Boolean,
        value: false,
      },
      _labelsChanged: {
        type: Boolean,
        value: false,
      },
      _saveTooltip: {
        type: String,
        value: ButtonTooltips.SAVE,
        readOnly: true,
      },
      _pluginMessage: {
        type: String,
        value: '',
      },
      _sendDisabled: {
        type: Boolean,
        computed: '_computeSendButtonDisabled(_sendButtonLabel, ' +
          'draftCommentThreads, draft, _reviewersMutated, _labelsChanged, ' +
          '_includeComments, disabled)',
        observer: '_sendDisabledChanged',
      },
      draftCommentThreads: {
        type: Array,
        observer: '_handleHeightChanged',
      },
    };
  }

  get keyBindings() {
    return {
      'esc': '_handleEscKey',
      'ctrl+enter meta+enter': '_handleEnterKey',
    };
  }

  static get observers() {
    return [
      '_changeUpdated(change.reviewers.*, change.owner)',
      '_ccsChanged(_ccs.splices)',
      '_reviewersChanged(_reviewers.splices)',
    ];
  }

  /** @override */
  attached() {
    super.attached();
    this._getAccount().then(account => {
      this._account = account || {};
    });
  }

  /** @override */
  ready() {
    super.ready();
    this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
  }

  open(opt_focusTarget) {
    this.knownLatestState = LatestPatchState.CHECKING;
    this.fetchChangeUpdates(this.change, this.$.restAPI)
        .then(result => {
          this.knownLatestState = result.isLatest ?
            LatestPatchState.LATEST : LatestPatchState.NOT_LATEST;
        });

    this._focusOn(opt_focusTarget);
    if (this.quote && this.quote.length) {
      // If a reply quote has been provided, use it and clear the property.
      this.draft = this.quote;
      this.quote = '';
    } else {
      // Otherwise, check for an unsaved draft in localstorage.
      this.draft = this._loadStoredDraft();
    }
    if (this.$.restAPI.hasPendingDiffDrafts()) {
      this._savingComments = true;
      this.$.restAPI.awaitPendingDiffDrafts().then(() => {
        this.fire('comment-refresh');
        this._savingComments = false;
      });
    }
  }

  focus() {
    this._focusOn(FocusTarget.ANY);
  }

  getFocusStops() {
    const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton;
    return {
      start: this.$.reviewers.focusStart,
      end,
    };
  }

  setLabelValue(label, value) {
    const selectorEl =
        this.$.labelScores.shadowRoot
            .querySelector(`gr-label-score-row[name="${label}"]`);
    if (!selectorEl) { return; }
    selectorEl.setSelectedValue(value);
  }

  getLabelValue(label) {
    const selectorEl =
        this.$.labelScores.shadowRoot
            .querySelector(`gr-label-score-row[name="${label}"]`);
    if (!selectorEl) { return null; }

    return selectorEl.selectedValue;
  }

  _handleEscKey(e) {
    this.cancel();
  }

  _handleEnterKey(e) {
    this._submit();
  }

  _ccsChanged(splices) {
    this._reviewerTypeChanged(splices, ReviewerTypes.CC);
  }

  _reviewersChanged(splices) {
    this._reviewerTypeChanged(splices, ReviewerTypes.REVIEWER);
  }

  _reviewerTypeChanged(splices, reviewerType) {
    if (splices && splices.indexSplices) {
      this._reviewersMutated = true;
      this._processReviewerChange(splices.indexSplices,
          reviewerType);
      let key;
      let index;
      let account;
      // Remove any accounts that already exist as a CC for reviewer
      // or vice versa.
      const isReviewer = ReviewerTypes.REVIEWER === reviewerType;
      for (const splice of splices.indexSplices) {
        for (let i = 0; i < splice.addedCount; i++) {
          account = splice.object[splice.index + i];
          key = this._accountOrGroupKey(account);
          const array = isReviewer ? this._ccs : this._reviewers;
          index = array.findIndex(
              account => this._accountOrGroupKey(account) === key);
          if (index >= 0) {
            this.splice(isReviewer ? '_ccs' : '_reviewers', index, 1);
            const moveFrom = isReviewer ? 'CC' : 'reviewer';
            const moveTo = isReviewer ? 'reviewer' : 'CC';
            const message = (account.name || account.email || key) +
                ` moved from ${moveFrom} to ${moveTo}.`;
            this.fire('show-alert', {message});
          }
        }
      }
    }
  }

  _processReviewerChange(indexSplices, type) {
    for (const splice of indexSplices) {
      for (const account of splice.removed) {
        if (!this._reviewersPendingRemove[type]) {
          console.err('Invalid type ' + type + ' for reviewer.');
          return;
        }
        this._reviewersPendingRemove[type].push(account);
      }
    }
  }

  /**
   * Resets the state of the _reviewersPendingRemove object, and removes
   * accounts if necessary.
   *
   * @param {boolean} isCancel true if the action is a cancel.
   * @param {Object=} opt_accountIdsTransferred map of account IDs that must
   *     not be removed, because they have been readded in another state.
   */
  _purgeReviewersPendingRemove(isCancel, opt_accountIdsTransferred) {
    let reviewerArr;
    const keep = opt_accountIdsTransferred || {};
    for (const type in this._reviewersPendingRemove) {
      if (this._reviewersPendingRemove.hasOwnProperty(type)) {
        if (!isCancel) {
          reviewerArr = this._reviewersPendingRemove[type];
          for (let i = 0; i < reviewerArr.length; i++) {
            if (!keep[reviewerArr[i]._account_id]) {
              this._removeAccount(reviewerArr[i], type);
            }
          }
        }
        this._reviewersPendingRemove[type] = [];
      }
    }
  }

  /**
   * Removes an account from the change, both on the backend and the client.
   * Does nothing if the account is a pending addition.
   *
   * @param {!Object} account
   * @param {string} type
   */
  _removeAccount(account, type) {
    if (account._pendingAdd) { return; }

    return this.$.restAPI.removeChangeReviewer(this.change._number,
        account._account_id).then(response => {
      if (!response.ok) { return response; }

      const reviewers = this.change.reviewers[type] || [];
      for (let i = 0; i < reviewers.length; i++) {
        if (reviewers[i]._account_id == account._account_id) {
          this.splice(`change.reviewers.${type}`, i, 1);
          break;
        }
      }
    });
  }

  _mapReviewer(reviewer) {
    let reviewerId;
    let confirmed;
    if (reviewer.account) {
      reviewerId = reviewer.account._account_id || reviewer.account.email;
    } else if (reviewer.group) {
      reviewerId = reviewer.group.id;
      confirmed = reviewer.group.confirmed;
    }
    return {reviewer: reviewerId, confirmed};
  }

  send(includeComments, startReview) {
    this.$.reporting.time(SEND_REPLY_TIMING_LABEL);
    const labels = this.$.labelScores.getLabelValues();

    const obj = {
      drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP',
      labels,
    };

    if (startReview) {
      obj.ready = true;
    }

    if (this.draft != null) {
      obj.message = this.draft;
    }

    const accountAdditions = {};
    obj.reviewers = this.$.reviewers.additions().map(reviewer => {
      if (reviewer.account) {
        accountAdditions[reviewer.account._account_id] = true;
      }
      return this._mapReviewer(reviewer);
    });
    const ccsEl = this.$.ccs;
    if (ccsEl) {
      for (let reviewer of ccsEl.additions()) {
        if (reviewer.account) {
          accountAdditions[reviewer.account._account_id] = true;
        }
        reviewer = this._mapReviewer(reviewer);
        reviewer.state = 'CC';
        obj.reviewers.push(reviewer);
      }
    }

    this.disabled = true;

    const errFn = this._handle400Error.bind(this);
    return this._saveReview(obj, errFn)
        .then(response => {
          if (!response) {
            // Null or undefined response indicates that an error handler
            // took responsibility, so just return.
            return {};
          }
          if (!response.ok) {
            this.fire('server-error', {response});
            return {};
          }

          this.draft = '';
          this._includeComments = true;
          this.fire('send', null, {bubbles: false});
          return accountAdditions;
        })
        .then(result => {
          this.disabled = false;
          return result;
        })
        .catch(err => {
          this.disabled = false;
          throw err;
        });
  }

  _focusOn(section) {
    // Safeguard- always want to focus on something.
    if (!section || section === FocusTarget.ANY) {
      section = this._chooseFocusTarget();
    }
    if (section === FocusTarget.BODY) {
      const textarea = this.$.textarea;
      textarea.async(textarea.getNativeTextarea()
          .focus.bind(textarea.getNativeTextarea()));
    } else if (section === FocusTarget.REVIEWERS) {
      const reviewerEntry = this.$.reviewers.focusStart;
      reviewerEntry.async(reviewerEntry.focus);
    } else if (section === FocusTarget.CCS) {
      const ccEntry = this.$.ccs.focusStart;
      ccEntry.async(ccEntry.focus);
    }
  }

  _chooseFocusTarget() {
    // If we are the owner and the reviewers field is empty, focus on that.
    if (this._account && this.change && this.change.owner &&
        this._account._account_id === this.change.owner._account_id &&
        (!this._reviewers || this._reviewers.length === 0)) {
      return FocusTarget.REVIEWERS;
    }

    // Default to BODY.
    return FocusTarget.BODY;
  }

  _handle400Error(response) {
    // A call to _saveReview could fail with a server error if erroneous
    // reviewers were requested. This is signalled with a 400 Bad Request
    // status. The default gr-rest-api-interface error handling would
    // result in a large JSON response body being displayed to the user in
    // the gr-error-manager toast.
    //
    // We can modify the error handling behavior by passing this function
    // through to restAPI as a custom error handling function. Since we're
    // short-circuiting restAPI we can do our own response parsing and fire
    // the server-error ourselves.
    //
    this.disabled = false;

    // Using response.clone() here, because getResponseObject() and
    // potentially the generic error handler will want to call text() on the
    // response object, which can only be done once per object.
    const jsonPromise = this.$.restAPI.getResponseObject(response.clone());
    return jsonPromise.then(result => {
      // Only perform custom error handling for 400s and a parseable
      // ReviewResult response.
      if (response.status === 400 && result) {
        const errors = [];
        for (const state of ['reviewers', 'ccs']) {
          if (!result.hasOwnProperty(state)) { continue; }
          for (const reviewer of Object.values(result[state])) {
            if (reviewer.error) {
              errors.push(reviewer.error);
            }
          }
        }
        response = {
          ok: false,
          status: response.status,
          text() { return Promise.resolve(errors.join(', ')); },
        };
      }
      this.fire('server-error', {response});
      return null; // Means that the error has been handled.
    });
  }

  _computeHideDraftList(draftCommentThreads) {
    return draftCommentThreads.length === 0;
  }

  _computeDraftsTitle(draftCommentThreads) {
    const total = draftCommentThreads.length;
    if (total == 0) { return ''; }
    if (total == 1) { return '1 Draft'; }
    if (total > 1) { return total + ' Drafts'; }
  }

  _computeMessagePlaceholder(canBeStarted) {
    return canBeStarted ?
      'Add a note for your reviewers...' :
      'Say something nice...';
  }

  _changeUpdated(changeRecord, owner) {
    // Polymer 2: check for undefined
    if ([changeRecord, owner].some(arg => arg === undefined)) {
      return;
    }

    this._rebuildReviewerArrays(changeRecord.base, owner);
  }

  _rebuildReviewerArrays(change, owner) {
    this._owner = owner;

    const reviewers = [];
    const ccs = [];

    for (const key in change) {
      if (change.hasOwnProperty(key)) {
        if (key !== 'REVIEWER' && key !== 'CC') {
          console.warn('unexpected reviewer state:', key);
          continue;
        }
        for (const entry of change[key]) {
          if (entry._account_id === owner._account_id) {
            continue;
          }
          switch (key) {
            case 'REVIEWER':
              reviewers.push(entry);
              break;
            case 'CC':
              ccs.push(entry);
              break;
          }
        }
      }
    }

    this._ccs = ccs;
    this._reviewers = reviewers;
  }

  _accountOrGroupKey(entry) {
    return entry.id || entry._account_id;
  }

  /**
   * Generates a function to filter out reviewer/CC entries. When isCCs is
   * truthy, the function filters out entries that already exist in this._ccs.
   * When falsy, the function filters entries that exist in this._reviewers.
   *
   * @param {boolean} isCCs
   * @return {!Function}
   */
  _filterReviewerSuggestionGenerator(isCCs) {
    return suggestion => {
      let entry;
      if (suggestion.account) {
        entry = suggestion.account;
      } else if (suggestion.group) {
        entry = suggestion.group;
      } else {
        console.warn(
            'received suggestion that was neither account nor group:',
            suggestion);
      }
      if (entry._account_id === this._owner._account_id) {
        return false;
      }

      const key = this._accountOrGroupKey(entry);
      const finder = entry => this._accountOrGroupKey(entry) === key;
      if (isCCs) {
        return this._ccs.find(finder) === undefined;
      }
      return this._reviewers.find(finder) === undefined;
    };
  }

  _getAccount() {
    return this.$.restAPI.getAccount();
  }

  _cancelTapHandler(e) {
    e.preventDefault();
    this.cancel();
  }

  cancel() {
    this.fire('cancel', null, {bubbles: false});
    this.$.textarea.closeDropdown();
    this._purgeReviewersPendingRemove(true);
    this._rebuildReviewerArrays(this.change.reviewers, this._owner);
  }

  _saveClickHandler(e) {
    e.preventDefault();
    if (!this.$.ccs.submitEntryText()) {
      // Do not proceed with the save if there is an invalid email entry in
      // the text field of the CC entry.
      return;
    }
    this.send(this._includeComments, false).then(keepReviewers => {
      this._purgeReviewersPendingRemove(false, keepReviewers);
    });
  }

  _sendTapHandler(e) {
    e.preventDefault();
    this._submit();
  }

  _submit() {
    if (!this.$.ccs.submitEntryText()) {
      // Do not proceed with the send if there is an invalid email entry in
      // the text field of the CC entry.
      return;
    }
    if (this._sendDisabled) {
      this.dispatchEvent(new CustomEvent('show-alert', {
        bubbles: true,
        composed: true,
        detail: {message: EMPTY_REPLY_MESSAGE},
      }));
      return;
    }
    return this.send(this._includeComments, this.canBeStarted)
        .then(keepReviewers => {
          this._purgeReviewersPendingRemove(false, keepReviewers);
        })
        .catch(err => {
          this.dispatchEvent(new CustomEvent('show-error', {
            bubbles: true,
            composed: true,
            detail: {message: `Error submitting review ${err}`},
          }));
        });
  }

  _saveReview(review, opt_errFn) {
    return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
        review, opt_errFn);
  }

  _reviewerPendingConfirmationUpdated(reviewer) {
    if (reviewer === null) {
      this.$.reviewerConfirmationOverlay.close();
    } else {
      this._pendingConfirmationDetails =
          this._ccPendingConfirmation || this._reviewerPendingConfirmation;
      this.$.reviewerConfirmationOverlay.open();
    }
  }

  _confirmPendingReviewer() {
    if (this._ccPendingConfirmation) {
      this.$.ccs.confirmGroup(this._ccPendingConfirmation.group);
      this._focusOn(FocusTarget.CCS);
    } else {
      this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group);
      this._focusOn(FocusTarget.REVIEWERS);
    }
  }

  _cancelPendingReviewer() {
    this._ccPendingConfirmation = null;
    this._reviewerPendingConfirmation = null;

    const target =
        this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS;
    this._focusOn(target);
  }

  _getStorageLocation() {
    // Tests trigger this method without setting change.
    if (!this.change) { return {}; }
    return {
      changeNum: this.change._number,
      patchNum: '@change',
      path: '@change',
    };
  }

  _loadStoredDraft() {
    const draft = this.$.storage.getDraftComment(this._getStorageLocation());
    return draft ? draft.message : '';
  }

  _handleAccountTextEntry() {
    // When either of the account entries has input added to the autocomplete,
    // it should trigger the save button to enable/
    //
    // Note: if the text is removed, the save button will not get disabled.
    this._reviewersMutated = true;
  }

  _draftChanged(newDraft, oldDraft) {
    this.debounce('store', () => {
      if (!newDraft.length && oldDraft) {
        // If the draft has been modified to be empty, then erase the storage
        // entry.
        this.$.storage.eraseDraftComment(this._getStorageLocation());
      } else if (newDraft.length) {
        this.$.storage.setDraftComment(this._getStorageLocation(),
            this.draft);
      }
    }, STORAGE_DEBOUNCE_INTERVAL_MS);
  }

  _handleHeightChanged(e) {
    this.fire('autogrow');
  }

  _handleLabelsChanged() {
    this._labelsChanged = Object.keys(
        this.$.labelScores.getLabelValues()).length !== 0;
  }

  _isState(knownLatestState, value) {
    return knownLatestState === value;
  }

  _reload() {
    // Load the current change without any patch range.
    Gerrit.Nav.navigateToChange(this.change);
    this.cancel();
  }

  _computeSendButtonLabel(canBeStarted) {
    return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND;
  }

  _computeSendButtonTooltip(canBeStarted) {
    return canBeStarted ? ButtonTooltips.START_REVIEW : ButtonTooltips.SEND;
  }

  _computeSavingLabelClass(savingComments) {
    return savingComments ? 'saving' : '';
  }

  _computeSendButtonDisabled(
      buttonLabel, draftCommentThreads, text, reviewersMutated,
      labelsChanged, includeComments, disabled) {
    // Polymer 2: check for undefined
    if ([
      buttonLabel,
      draftCommentThreads,
      text,
      reviewersMutated,
      labelsChanged,
      includeComments,
      disabled,
    ].some(arg => arg === undefined)) {
      return undefined;
    }

    if (disabled) { return true; }
    if (buttonLabel === ButtonLabels.START_REVIEW) { return false; }
    const hasDrafts = includeComments && draftCommentThreads.length;
    return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged;
  }

  _computePatchSetWarning(patchNum, labelsChanged) {
    let str = `Patch ${patchNum} is not latest.`;
    if (labelsChanged) {
      str += ' Voting on a non-latest patch will have no effect.';
    }
    return str;
  }

  setPluginMessage(message) {
    this._pluginMessage = message;
  }

  _sendDisabledChanged(sendDisabled) {
    this.dispatchEvent(new CustomEvent('send-disabled-changed'));
  }

  _getReviewerSuggestionsProvider(change) {
    const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
        change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
    provider.init();
    return provider;
  }

  _getCcSuggestionsProvider(change) {
    const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI,
        change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC);
    provider.init();
    return provider;
  }

  _onThreadListModified() {
    // TODO(taoalpha): this won't propogate the changes to the files
    // should consider replacing this with either top level events
    // or gerrit level events

    // emit the event so change-view can also get updated with latest changes
    this.fire('comment-refresh');
  }
}

customElements.define(GrReplyDialog.is, GrReplyDialog);
